package org.restql.webapp;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.prefs.Preferences;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.restql.db.operation.DatabaseDelete;
import org.restql.db.operation.DatabaseDtd;
import org.restql.db.operation.DatabaseGet;
import org.restql.db.operation.DatabaseInsert;
import org.restql.db.operation.DatabaseSelect;
import org.restql.db.operation.DatabaseUpdate;
import org.restql.db.resource.DatabaseResource;
import org.restql.db.resource.ModifiableDatabaseResource;
import org.restql.db.resource.ModifiableDatabaseResourceImpl;
import org.restql.db.resource.RequestResource;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;

public class RestQLServlet extends HttpServlet {

	private static final long serialVersionUID = 1L;
	private static Log logger = LogFactory.getLog(RestQLServlet.class);
	private Preferences userPrefs;
	private RestQLResourceContainer resourceContainer = null;
	private static List<RestQLServlet> allServletInstance = new ArrayList<RestQLServlet>();
	private String configFilePath = null;
	
	public RestQLServlet() { }
	
	public RestQLServlet(String configFilePath) {
		this.configFilePath = configFilePath;
	}
	
	public void init(ServletConfig servletConfig) {
		logger.info("Calling init");
		userPrefs = Preferences.userNodeForPackage(RestQLServlet.class);
		allServletInstance.add(this);
		applyConfig();
	}

	public static void applyConfigToAll() {
		logger.info("Reapplying configuration.");
		for (RestQLServlet rqls : allServletInstance) {
			rqls.applyConfig();
		}
	}

	protected void applyConfig() {
		String configType = userPrefs.get("configType", "auto");
		try {
			AbstractResource res = null;
			
			if (configFilePath != null) {
				logger.info("Using config file specified at launch: " + configFilePath);
				res = new FileSystemResource(configFilePath);
			} else if (configType.equals("auto")) {
				logger.info("Using auto config.");
				res = new ClassPathResource("restqlResources.xml");
			} else if (configType.equals("file")){
				logger.info("Using file config.");
				String configLocation = userPrefs.get("configLocation", "");
				res = new FileSystemResource(configLocation);
			}

			XmlBeanFactory factory = new XmlBeanFactory(res);
			this.resourceContainer = (RestQLResourceContainer) factory.getBean("restQLResourceContainer");
			
			// Auto configure the config based on User preferences
			if (configType.equals("auto")) {
				ModifiableDatabaseResourceImpl dbResource = 
					(ModifiableDatabaseResourceImpl) this.resourceContainer.getResources().get(0);
				if (userPrefs.get("allowGet", "true").equals("false")) {
					dbResource.setAllowSelect(false);
				}
				if (userPrefs.get("allowPut", "true").equals("false")) {
					dbResource.setAllowInsert(false);
				}
				if (userPrefs.get("allowPost", "true").equals("false")) {
					dbResource.setAllowUpdate(false);
				}
				if (userPrefs.get("allowDelete", "true").equals("false")) {
					dbResource.setAllowDelete(false);
				}
			}
			
		} catch (Exception e) {
			logger.error(e);
		}
	}

	public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {

		String requestedPath = request.getPathInfo();

		// Get user to configure war to start things out -- disable for now
		if (false && userPrefs.get("configLocation", null) == null) {
			//RequestDispatcher view = request.getRequestDispatcher("/config.jsp");
			logger.info("Forwarding to config page");
			//view.forward(request, response);
			response.sendRedirect(request.getContextPath() + "/config.jsp");
			return;
		}

		// Just return an info page for a request to root
		if (requestedPath.equals("/")) {
			RequestDispatcher view = request.getRequestDispatcher("/intro.jsp");
			view.forward(request, response);
			return;
		}

		String []pathElements = requestedPath.split("/");

		if (! validatePath(pathElements)) {
			generateErrorResponse(response, "Bad path requested: " + requestedPath);
		} else {

			Map params = new HashMap();
			for (Object k : request.getParameterMap().keySet()) {
				params.put(k, request.getParameterMap().get(k));
			}
			String dbName = pathElements[1];
			String tableName = pathElements[2];
			String keyField = null;
			String keyValue = null;
			if (pathElements.length == 5) {
				keyField = pathElements[3];
				keyValue = pathElements[4];
				
				params.put(keyField, new String[] { (String) keyValue} );
			}
			ReturnType retType = ReturnType.xml;

			if (tableName.endsWith(".dtd")) {
				tableName = tableName.substring(0, tableName.length() - 4);
				retType = ReturnType.dtd;
			}

			AccessControl accessControl = new AccessControl(request, dbName, tableName, request.getMethod());
			if (! accessControl.operationAuthorized()) {
				generateErrorResponse(response, "Operation not authorized for this table.");
				return;
			}

			try {
				DatabaseResource databaseResource =
					resourceContainer.selectMatchingResource(dbName, tableName);

				if (databaseResource.allowSelect()) {
					DatabaseGet get = null;
					if (retType == ReturnType.xml) {
						get = new DatabaseSelect(databaseResource, new RequestResource(dbName, tableName, accessControl.getUser(),
								accessControl.getPassword(), params));
					} else if (retType == ReturnType.dtd) {
						get = new DatabaseDtd(databaseResource, new RequestResource(dbName, tableName, accessControl.getUser(),
								accessControl.getPassword(), params));
					}
					get.executeSql(response.getWriter());
					response.setContentType(retType.contentType());
				} else {
					generateErrorResponse(response, "Select not allowed for this resource.");
				}
			} catch (RestQLException e) {
				if (e.getAccessDenied()) {
					generateAccessErrorResponse(response, dbName);
				} else {
					generateErrorResponse(response, e.toString());
				}
			}

		}
	}

	public void doPut(HttpServletRequest request, HttpServletResponse response) throws IOException {
		String requestedPath = request.getPathInfo();
		String []pathElements = requestedPath.split("/");

		if (! validatePath(pathElements)) {
			generateErrorResponse(response, "Bad path requested: " + requestedPath);
		} else {

			String dbName = pathElements[1];
			String tableName = pathElements[2];

			AccessControl accessControl = new AccessControl(request, dbName, tableName, request.getMethod());
			if (! accessControl.operationAuthorized()) {
				generateErrorResponse(response, "Operation not authorized for this table.");
				return;
			}

			try {
				DatabaseResource databaseResource =
					resourceContainer.selectMatchingResource(dbName, tableName);

				if (databaseResource.allowInsert() && databaseResource instanceof ModifiableDatabaseResource) {
					DatabaseInsert insert = new DatabaseInsert(databaseResource, new RequestResource(dbName, tableName, accessControl.getUser(),
							accessControl.getPassword(), request.getParameterMap()));
					insert.executeSql(request.getInputStream());
					response.setContentType("text/xml");
					PrintWriter out = response.getWriter();
					out.println("<?xml version='1.0' ?><insert><message>OK</message></insert>");
				} else {
					generateErrorResponse(response, "Insert not supported for this resource.");
				}
			} catch (RestQLException e) {
				if (e.getAccessDenied()) {
					generateAccessErrorResponse(response, dbName);
				} else {
					generateErrorResponse(response, e.toString());
				}
			}
		}
	}

	public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
		String requestedPath = request.getPathInfo();
		String []pathElements = requestedPath.split("/");

		if (requestedPath.equals("/postConfig")) {
			ConfigProcessor.processPostedConfig(request, response);
			return;
		}

		if (! validatePath(pathElements)) {
			generateErrorResponse(response, "Bad path requested: " + requestedPath);
		} else {

			String dbName = pathElements[1];
			String tableName = pathElements[2];

			AccessControl accessControl = new AccessControl(request, dbName, tableName, request.getMethod());
			if (! accessControl.operationAuthorized()) {
				generateErrorResponse(response, "Operation not authorized for this table.");
				return;
			}

			try {
				DatabaseResource databaseResource =
					resourceContainer.selectMatchingResource(dbName, tableName);

				if (databaseResource.allowUpdate() && databaseResource instanceof ModifiableDatabaseResource) {
					DatabaseUpdate update = new DatabaseUpdate(databaseResource, new RequestResource(dbName, tableName, accessControl.getUser(),
							accessControl.getPassword(), request.getParameterMap()));

					update.executeSql(request.getInputStream());
					response.setContentType("text/xml");
					PrintWriter out = response.getWriter();
					out.println("<?xml version='1.0' ?><update><message>OK</update></insert>");
				} else {
					generateErrorResponse(response, "Updates not allowed for this resource.");
				}
			} catch (RestQLException e) {
				if (e.getAccessDenied()) {
					generateAccessErrorResponse(response, dbName);
				} else {
					generateErrorResponse(response, e.toString());
				}
			}
		}
	}

	public void doDelete(HttpServletRequest request, HttpServletResponse response) throws IOException {
		String requestedPath = request.getPathInfo();
		String []pathElements = requestedPath.split("/");

		if (! validatePath(pathElements)) {
			generateErrorResponse(response, "Bad path requested: " + requestedPath);
		} else {

			String dbName = pathElements[1];
			String tableName = pathElements[2];

			AccessControl accessControl = new AccessControl(request, dbName, tableName, request.getMethod());
			if (! accessControl.operationAuthorized()) {
				generateErrorResponse(response, "Operation not authorized for this table.");
				return;
			}

			try {
				DatabaseResource databaseResource =
					resourceContainer.selectMatchingResource(dbName, tableName);

				if (databaseResource.allowDelete() && databaseResource instanceof ModifiableDatabaseResource) {
					DatabaseDelete delete = new DatabaseDelete(databaseResource, new RequestResource(dbName, tableName, accessControl.getUser(),
							accessControl.getPassword(), request.getParameterMap()));
					delete.executeSql(request.getInputStream());
					response.setContentType("text/xml");
					PrintWriter out = response.getWriter();
					out.println("<?xml version='1.0' ?><update><message>OK</update></insert>");
				} else {
					generateErrorResponse(response, "Delete not allowed for this resource.");
				}
			} catch (RestQLException e) {
				if (e.getAccessDenied()) {
					generateAccessErrorResponse(response, dbName);
				} else {
					generateErrorResponse(response, e.toString());
				}
			}
		}
	}

	private void generateAccessErrorResponse(HttpServletResponse response, String dbName) throws IOException {
		response.setContentType("text/xml");
		PrintWriter out = response.getWriter();
		out.println("<?xml version='1.0' ?><error><message>Authentication required for: " + dbName + "</message></error>");
		response.setHeader("WWW-Authenticate", "Basic realm=\"restql:" + dbName + "\"");
		response.setStatus(401);
	}

	protected boolean validatePath(String []splitPath) {
		if (splitPath.length == 3 || splitPath.length == 5) return true;
		else return false;
	}

	protected void generateErrorResponse(HttpServletResponse response, String message) throws IOException {
		response.setContentType("text/xml");
		PrintWriter out = response.getWriter();
		out.println("<?xml version='1.0' ?><error><message>" + message + "</message></error>");
		response.setStatus(400);
	}

	private enum ReturnType {
		xml ("text/xml"),
		dtd ("text/plain"),
		xsd ("text/xsd");

		private final String contentType;

		ReturnType(String s) {
			this.contentType = s;
		}

		public String contentType() { return this.contentType; }
	};
}